#!/usr/bin/env ruby
$VERBOSE = nil

# Copyright (C) 2006-2008 by Sergio Pistone
# sergio_pistone@yahoo.com.ar
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

require File.expand_path( File.dirname( __FILE__ ) + "/../utils/strings" )
require File.expand_path( File.dirname( __FILE__ ) + "/../utils/xmlhash" )
require File.expand_path( File.dirname( __FILE__ ) + "/../utils/logger" )
require File.expand_path( File.dirname( __FILE__ ) + "/../utils/kde" )
require File.expand_path( File.dirname( __FILE__ ) + "/../i18n/i18n" )
require File.expand_path( File.dirname( __FILE__ ) + "/../gui/gui" )
require File.expand_path( File.dirname( __FILE__ ) + "/../lyrics" )
require File.expand_path( File.dirname( __FILE__ ) + "/amarok" )
require File.expand_path( File.dirname( __FILE__ ) + "/plugins" )

require "cgi"
require "thread"

$TOOLKIT_PRIORITY = ["qt3", "qt4", "gtk", "tk"]
$LOG_FILEPATH = "#{ENV["HOME"]}/.wikilyrics.log"
$LOG_TRUNCATE = 2000

def debug( message )
	Amarok.popup( message.to_s() )
end

class Application

	@@MENU_ENTRY_SEARCH_CURRENT = I18n.get( "amarok.application.search.current" )
	@@MENU_ENTRY_SEARCH_SELECTED = I18n.get( "amarok.application.search.selected" )
	@@MENU_ENTRY_CLEAR_LYRICS_CACHE = I18n.get( "amarok.application.clearlyricscache" )

	@@SCRIPT_NAME = "Wiki-Lyrics"
	@@CONFIG_FILE = "wikilyrics.xml"

	def initialize()
		self.logger = nil
		self.cleanup_lyrics = true
		self.single_threaded = false
	end

	def finalize()
		@logger.finalize() if @logger
	end

	def log?()
		return @logger != nil
	end

	def logger=( logger )
		@logger = logger
		Plugins.all_plugins().each() { |plugin| plugin.logger = @logger }
	end

	def log( message, new_lines=1 )
		@logger.log( message, new_lines ) if @logger
	end

	def cleanup_lyrics?()
		return @cleanup_lyrics
	end

	def cleanup_lyrics=( cleanup_lyrics )
		@cleanup_lyrics = cleanup_lyrics
		Plugins.all_plugins().each() { |plugin| plugin.cleanup_lyrics = cleanup_lyrics }
	end

	def single_threaded?()
		return @single_threaded
	end

	def single_threaded=( single_threaded )
		@single_threaded = single_threaded
	end

	def notify( message )
		Amarok.notify( "<b>[" + @@SCRIPT_NAME + "]</b> " + message.gsub( "\n", " " ) )
	end

	def popup( message )
		Amarok.popup( "<b>[" + @@SCRIPT_NAME + "]</b><br/>" + message )
	end

	def add_custom_menu_item( menu_item )
		Amarok.add_custom_menu_item( @@SCRIPT_NAME, menu_item )
	end

	def remove_custom_menu_item( menu_item )
		Amarok.remove_custom_menu_item( @@SCRIPT_NAME, menu_item )
	end

	def read_config()
		values = {
			"cleanup_lyrics" => cleanup_lyrics?(),
			"single_threaded" => single_threaded?(),
			"write_log" => log?,
			"used_plugins" => Plugins.used_names()
		}
		XMLHash.read( @@CONFIG_FILE, values )
		self.logger = values["write_log"].to_s() == "true" ? Logger.new( $LOG_FILEPATH, $LOG_TRUNCATE ) : nil
		self.cleanup_lyrics = values["cleanup_lyrics"].to_s() == "true"
		self.single_threaded = values["single_threaded"].to_s() == "true"
		Plugins.used_names = values["used_plugins"]
		Plugins.all_plugins().each() { |plugin| plugin.read_config( @@CONFIG_FILE ) }
	end

	def write_config()
		values = {
			"cleanup_lyrics" => cleanup_lyrics?(),
			"single_threaded" => single_threaded?(),
			"write_log" => log?,
			"used_plugins" => Plugins.used_names()
		}
		XMLHash.write( @@CONFIG_FILE, values )
		Plugins.all_plugins().each() { |plugin| plugin.write_config( @@CONFIG_FILE ) }
	end


	# EVENT HANDLERS

	def on_start()
		add_custom_menu_item( @@MENU_ENTRY_SEARCH_CURRENT )
		add_custom_menu_item( @@MENU_ENTRY_SEARCH_SELECTED )
		add_custom_menu_item( @@MENU_ENTRY_CLEAR_LYRICS_CACHE )
		Plugins.all_plugins().each() { |plugin| plugin.on_start() }
	end

	def on_quit()
		remove_custom_menu_item( @@MENU_ENTRY_SEARCH_CURRENT )
		remove_custom_menu_item( @@MENU_ENTRY_SEARCH_SELECTED )
		remove_custom_menu_item( @@MENU_ENTRY_CLEAR_LYRICS_CACHE )
		Plugins.all_plugins().each() { |plugin| plugin.on_quit() }
	end

	def on_configure()

		values = {
			"cleanup_lyrics" => cleanup_lyrics?(),
			"single_threaded" => single_threaded?(),
			"write_log" => log?,
			"used_plugins" => Plugins.used_names(),
			"script_name" => @@SCRIPT_NAME,
			"unused_plugins" => Plugins.all_names() - Plugins.used_names()
		}
		if GUI.show_plugins_manager_dialog( values )
			self.logger = values["write_log"].to_s() == "true" ? Logger.new( $LOG_FILEPATH, $LOG_TRUNCATE ) : nil
			self.cleanup_lyrics = values["cleanup_lyrics"].to_s() == "true"
			self.single_threaded = values["single_threaded"].to_s() == "true"
			Plugins.used_names = values["used_plugins"]
		end
		Plugins.all_plugins().each() { |plugin| plugin.configure() }

		write_config()

	end


	def on_search_current_lyrics()
		if Amarok.playing?()
			song_data = Amarok.query_song_data( Amarok.get_current_path() )
			if GUI.show_search_lyrics_dialog( song_data )
				on_fetch_lyrics( Lyrics::Request.new(
					song_data["artist"],
					song_data["title"],
					song_data["album"],
					song_data["year"]
				) )
			end
		else
			notify( I18n.get( "amarok.application.search.current.nosongplaying" ) )
		end
	end

	def on_search_file_lyrics( file )
		if (song_data = Amarok.query_song_data( file )).empty?
			notify( I18n.get( "amarok.application.search.noinfofound" ) )
		elsif GUI.show_search_lyrics_dialog( song_data )
			request = Lyrics::Request.new( song_data["artist"], song_data["title"], song_data["album"], song_data["year"] )
			response, plugin, suggestions = fetch_lyrics( request )
			if response.lyrics
				dialog_data = {
					"artist" =>		response.artist,
					"title" =>		response.title,
					"lyrics" =>		response.lyrics,
					"site_name" =>	plugin.site_name(),
				}
				GUI.show_lyrics_dialog( dialog_data )
			else
				notify( I18n.get( "amarok.application.search.nolyricsfound", song_data["title"], song_data["artist"] ) )
			end
		end
	end

	def on_custom_menu_item_selected( menu, item, urls )
		if menu == @@SCRIPT_NAME
			if item == @@MENU_ENTRY_SEARCH_CURRENT
				on_search_current_lyrics()
			elsif item == @@MENU_ENTRY_SEARCH_SELECTED
				url = URI.parse( urls[0] )
				return if url.scheme != "file"
				on_search_file_lyrics( URI.decode( url.path ) )
			elsif item == @@MENU_ENTRY_CLEAR_LYRICS_CACHE
				if GUI.show_confirmation_dialog( I18n.get( "amarok.application.clearlyricscache.confirm" ), @@SCRIPT_NAME )
					Amarok.query( "DELETE FROM lyrics WHERE url <> ''" )
					notify( I18n.get( "amarok.application.clearlyricscache.done" ) )
				end
			end
		elsif plugin = Plugins.plugin_by_name( menu )
			plugin.on_custom_menu_item_selected( menu, item, urls )
		end
	end

	def build_lyrics_data_hash( response, plugin=nil )
		return {
			"artist"	=> response.artist ? response.artist : response.request.artist,
			"title"		=> response.title ? response.title : response.request.title,
			"lyrics"	=> response.lyrics,
			"url"		=> response.url,
			"add_url"	=> plugin ? plugin.build_song_add_url( response.request ) : nil,
			"site_name"	=> plugin ? plugin.site_name : nil,
			"site_host"	=> plugin ? plugin.site_host : nil,
		}
	end
	protected :build_lyrics_data_hash

	def wikis_process_response( response, response_plugin, searched_wiki_plugins )
		Plugins.wiki_plugins().each() do |plugin|
			plugin.wiki_process_response( response, response_plugin, searched_wiki_plugins.include?( plugin ) )
		end
	end

	def fetch_lyrics( request )

		used_plugins = Plugins.used_plugins()

		response = nil
		response_plugin = nil
		suggestions = []

		if used_plugins.empty?
			popup( I18n.get( "amarok.application.error.nopluginsselected" ) )
			response = Lyrics::Response.new( request )
		else
			searched_wiki_plugins = []
			used_plugins.each() do |plugin|
				begin
					searched_wiki_plugins << plugin if plugin.is_a?( MediaWikiLyrics )
					response = plugin.lyrics_direct_search( request )
					if response.lyrics
						response_plugin = plugin
						break
					end
					response, suggs = plugin.lyrics_from_suggestions( request )
					if suggs.size > 0
						suggestions << plugin.plugin_name()
						suggestions.concat( suggs )
					end
					if response.lyrics
						response_plugin = plugin
						break
					end
				rescue TimeoutError
					notify( I18n.get( "amarok.application.search.plugintimeout", plugin.plugin_name(), plugin.site_host() ) )
				end
			end
			wikis_process_response( response, response_plugin, searched_wiki_plugins )
		end

		return response, response_plugin, suggestions

	end

	def on_fetch_lyrics( request )

		# NOTE: we must check that the current song doesn't change while we're fetching
		# the lyrics, otherwise Amarok will associate the lyrics with the wrong song
		prev_artist = Strings.normalize!( Amarok.get_current_artist().to_s() )
		prev_title  = Strings.normalize!( Amarok.get_current_title().to_s() )

		response, plugin, suggestions = fetch_lyrics( request )
		lyrics_data = build_lyrics_data_hash( response, plugin )

		return if Strings.normalize!( Amarok.get_current_artist().to_s() ) != prev_artist ||
				  Strings.normalize!( Amarok.get_current_title().to_s() ) != prev_title

		if lyrics_data["lyrics"]
			Amarok.show_lyrics(
				lyrics_data["artist"],
				lyrics_data["title"],
				lyrics_data["lyrics"],
				lyrics_data["site_name"],
				lyrics_data["site_host"],
				lyrics_data["url"],
				lyrics_data["add_url"]
			)
		elsif suggestions.size > 0
			Amarok.show_suggestions(
				lyrics_data["artist"],
				lyrics_data["title"],
				suggestions,
				lyrics_data["add_url"]
			)
		else
			Amarok.show_not_found(
				lyrics_data["artist"],
				lyrics_data["title"],
				lyrics_data["add_url"]
			)
		end

	end

	def fetch_lyrics_from_url( request, url )

		used_plugins = Plugins.used_plugins()

		response = nil
		response_plugin = nil

		if used_plugins.empty?
			popup( I18n.get( "amarok.application.error.nopluginsselected" ) )
			response = Lyrics::Response.new( request )
		else
			searched_wiki_plugins = []
			used_plugins.each() do |plugin|
				begin
					next if ! plugin.known_url?( url )
					searched_wiki_plugins << plugin if plugin.is_a?( MediaWikiLyrics )
					response = plugin.lyrics_from_url( request, url )
					if response.lyrics
						response_plugin = plugin
						break
					end
				rescue TimeoutError
					notify( I18n.get( "amarok.application.search.plugintimeout", plugin.plugin_name(), plugin.site_host() ) )
				end
			end
			wikis_process_response( response, response_plugin, searched_wiki_plugins )
		end

		return response, response_plugin
	end

	def on_fetch_lyrics_from_url( request, url )

		# NOTE: we must check that the current song doesn't change while we're fetching
		# the lyrics, otherwise Amarok will associate the lyrics with the wrong song
		prev_artist = Strings.normalize!( Amarok.get_current_artist().to_s() )
		prev_title  = Strings.normalize!( Amarok.get_current_title().to_s() )

		response, plugin = fetch_lyrics_from_url( request, url )
		lyrics_data = build_lyrics_data_hash( response, plugin )

		return if Strings.normalize!( Amarok.get_current_artist().to_s() ) != prev_artist ||
				  Strings.normalize!( Amarok.get_current_title().to_s() ) != prev_title

		if lyrics_data["lyrics"]
			Amarok.show_lyrics(
				lyrics_data["artist"],
				lyrics_data["title"],
				lyrics_data["lyrics"],
				lyrics_data["site_name"],
				lyrics_data["site_host"],
				lyrics_data["url"],
				lyrics_data["add_url"]
			)
		else
			Amarok.show_not_found(
				lyrics_data["artist"],
				lyrics_data["title"],
				lyrics_data["add_url"]
			)
		end

	end

	def exec()

		@threads = []
		@messages = []

		read_config()

		begin

			on_start()

			single_threaded = self.single_threaded?

			GUI.set_run_out_of_process( ! single_threaded )

			exec_messages_loop( single_threaded )

		rescue Errno::ECONNREFUSED => e
			popup( I18n.get( "amarok.application.error.connectionrefused" ) )
			popup( e.backtrace().join( "<br/>" ) )

		rescue SocketError, TimeoutError => e
			popup( I18n.get( "amarok.application.error.connectiondown" ) )

		rescue Exception => e # unexpected error
			if ! e.is_a?( SystemExit ) || e.status != 0
				$stderr << e.message() << "\n"
				$stderr << e.backtrace().join( "\n" )
				log( e.message() )
				log( e.backtrace().join( "\n" ), 0 )
				exit( 1 )
			end

		ensure
			write_config()
			on_quit()

		end

	end

	def exec_messages_loop( single_threaded )

		loop do
			message = get_message()
			break if message == nil
			command = /[A-Za-z]*/.match( message ).to_s()
			case command
				when "configure"
					on_configure()
				when "fetchLyrics"
					artist = Amarok.get_current_artist()
					title  = Amarok.get_current_title()
					if artist && title
						if single_threaded
							on_fetch_lyrics( Lyrics::Request.new(
								Strings.cleanup_artist( artist, title ),
								Strings.cleanup_title( title ),
								Amarok.get_current_album(),
								Amarok.get_current_year()
							) )
						else
							@threads << Thread.new() do
								on_fetch_lyrics( Lyrics::Request.new(
									Strings.cleanup_artist( artist, title ),
									Strings.cleanup_title( title ),
									Amarok.get_current_album(),
									Amarok.get_current_year()
								) )
							end
						end
					else
						Amarok.show_error()
					end
				when "fetchLyricsByUrl"
					args = message.split()
					if args.length < 2
						Amarok.show_error()
					else
						artist = Amarok.get_current_artist().to_s()
						title  = Amarok.get_current_title().to_s()
						if single_threaded
							on_fetch_lyrics_from_url(
								Lyrics::Request.new(
									Strings.cleanup_artist( artist, title ),
									Strings.cleanup_title( title ),
									Amarok.get_current_album(),
									Amarok.get_current_year()
								),
								args[1]
							)
						else
							@threads << Thread.new() do
								on_fetch_lyrics_from_url(
									Lyrics::Request.new(
										Strings.cleanup_artist( artist, title ),
										Strings.cleanup_title( title ),
										Amarok.get_current_album(),
										Amarok.get_current_year()
									),
									args[1]
								)
							end
						end
					end
				when "customMenuClicked"
					urls_start = message.index( /([a-zA-Z]+:\/\/[^: ]+){1,}/ )
					next if urls_start == nil # unexpected message
					menu_id = message.slice( "customMenuClicked:".size+1..urls_start-2 )
					menu, item = Amarok.get_custom_menu_item( menu_id )
					if menu && item
						urls = message.slice( urls_start..-1 ).split( " " )
						if single_threaded
							on_custom_menu_item_selected( menu, item, urls )
						else
							@threads << Thread.new() { on_custom_menu_item_selected( menu, item, urls ) }
						end
					end
			end
		end
	end
	protected :exec_messages_loop

	def get_message()
		begin
			loop do
				begin
					messages = $stdin.read_nonblock( 2048 ) # we read all there is to read
					messages.chomp().split( "\n" ).each() { |message| store_message( message ) }
				rescue Errno::EAGAIN # there was nothing to read
				rescue EOFError
					return nil # stdin was closed
				end
				if @messages.empty?
					message = $stdin.gets() # wait for something to be written to stdin
					store_message( message )
					begin
						messages = $stdin.read_nonblock( 2048 ) # if something else was written, we read it too
						messages.chomp().split( "\n" ).each() { |message| store_message( message ) }
					rescue Errno::EAGAIN # there was nothing to read
					rescue EOFError
						return nil # stdin was closed
					end
				end
				return @messages.shift() if ! @messages.empty?
			end
		rescue SignalException => e
			return nil
		end

	end
	protected :get_message

	def store_message( message )
		command = /[A-Za-z]*/.match( message ).to_s()
		if command == "configure"
			# prioritize configure messages:
			@messages.insert( 0, "configure" ) if @messages[0] != "configure"
		elsif ["fetchLyrics", "fetchLyricsByUrl", "customMenuClicked"].include?( command )
			# we queue only two messages to try to stay in synch with Amarok current track
			@messages[@messages.size < 2 ? @messages.size : 1 ] = message
		end
	end
	protected :store_message

end

if $0 == __FILE__

	if GUI.set_toolkit_priority( $TOOLKIT_PRIORITY ) == nil
		Amarok.popup( I18n.get( "amarok.application.error.notoolkit" ) )
		exit( 0 )
	end

	proxy_url, excluded_urls, reverse = KDE.get_proxy_settings()
	HTTP.set_proxy_settings( proxy_url, excluded_urls, reverse )

	app = Application.new()

	begin
		app.exec()
	ensure
		app.finalize()
	end

end
